From 1b4c656d49a486d135585748632bba365daead80 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 19 Feb 2026 15:49:43 +0100 Subject: [PATCH] [PATCH] http: use null prototype for headersDistinct/trailersDistinct Use { __proto__: null } instead of {} when initializing the headersDistinct and trailersDistinct destination objects. A plain {} inherits from Object.prototype, so when a __proto__ header is received, dest["__proto__"] resolves to Object.prototype (truthy), causing _addHeaderLineDistinct to call .push() on it, which throws an uncaught TypeError and crashes the process. Ref: https://hackerone.com/reports/3560402 PR-URL: https://github.com/nodejs-private/node-private/pull/821 Refs: https://hackerone.com/reports/3560402 Reviewed-By: Marco Ippolito Reviewed-By: Rafael Gonzaga CVE-ID: CVE-2026-21710 Gbp-Pq: Topic sec Gbp-Pq: Name 52-http-use-null-prototype-for-headersDistinct-trailersDistinct.patch --- lib/_http_incoming.js | 4 +-- .../test-http-headers-distinct-proto.js | 36 +++++++++++++++++++ test/parallel/test-http-multiple-headers.js | 16 ++++----- 3 files changed, 46 insertions(+), 10 deletions(-) create mode 100644 test/parallel/test-http-headers-distinct-proto.js diff --git a/lib/_http_incoming.js b/lib/_http_incoming.js index 1dd04fdf3..994a24f02 100644 --- a/lib/_http_incoming.js +++ b/lib/_http_incoming.js @@ -128,7 +128,7 @@ ObjectDefineProperty(IncomingMessage.prototype, 'headersDistinct', { __proto__: null, get: function() { if (!this[kHeadersDistinct]) { - this[kHeadersDistinct] = {}; + this[kHeadersDistinct] = { __proto__: null }; const src = this.rawHeaders; const dst = this[kHeadersDistinct]; @@ -168,7 +168,7 @@ ObjectDefineProperty(IncomingMessage.prototype, 'trailersDistinct', { __proto__: null, get: function() { if (!this[kTrailersDistinct]) { - this[kTrailersDistinct] = {}; + this[kTrailersDistinct] = { __proto__: null }; const src = this.rawTrailers; const dst = this[kTrailersDistinct]; diff --git a/test/parallel/test-http-headers-distinct-proto.js b/test/parallel/test-http-headers-distinct-proto.js new file mode 100644 index 000000000..bd4cb82bd --- /dev/null +++ b/test/parallel/test-http-headers-distinct-proto.js @@ -0,0 +1,36 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); +const net = require('net'); + +// Regression test: sending a __proto__ header must not crash the server +// when accessing req.headersDistinct or req.trailersDistinct. + +const server = http.createServer(common.mustCall((req, res) => { + const headers = req.headersDistinct; + assert.strictEqual(Object.getPrototypeOf(headers), null); + assert.deepStrictEqual(Object.getOwnPropertyDescriptor(headers, '__proto__').value, ['test']); + res.end(); +})); + +server.listen(0, common.mustCall(() => { + const port = server.address().port; + + const client = net.connect(port, common.mustCall(() => { + client.write( + 'GET / HTTP/1.1\r\n' + + 'Host: localhost\r\n' + + '__proto__: test\r\n' + + 'Connection: close\r\n' + + '\r\n', + ); + })); + + client.on('end', common.mustCall(() => { + server.close(); + })); + + client.resume(); +})); diff --git a/test/parallel/test-http-multiple-headers.js b/test/parallel/test-http-multiple-headers.js index f9c654ba2..bc49ba1e4 100644 --- a/test/parallel/test-http-multiple-headers.js +++ b/test/parallel/test-http-multiple-headers.js @@ -26,13 +26,13 @@ const server = createServer( host, 'transfer-encoding': 'chunked' }); - assert.deepStrictEqual(req.headersDistinct, { + assert.deepStrictEqual(req.headersDistinct, Object.assign({ __proto__: null }, { 'connection': ['close'], 'x-req-a': ['eee', 'fff', 'ggg', 'hhh'], 'x-req-b': ['iii; jjj; kkk; lll'], 'host': [host], - 'transfer-encoding': ['chunked'] - }); + 'transfer-encoding': ['chunked'], + })); req.on('end', function() { assert.deepStrictEqual(req.rawTrailers, [ @@ -45,7 +45,7 @@ const server = createServer( ); assert.deepStrictEqual( req.trailersDistinct, - { 'x-req-x': ['xxx', 'yyy'], 'x-req-y': ['zzz; www'] } + Object.assign({ __proto__: null }, { 'x-req-x': ['xxx', 'yyy'], 'x-req-y': ['zzz; www'] }) ); res.setHeader('X-Res-a', 'AAA'); @@ -132,14 +132,14 @@ server.listen(0, common.mustCall(() => { 'x-res-d': 'JJJ; KKK; LLL', 'transfer-encoding': 'chunked' }); - assert.deepStrictEqual(res.headersDistinct, { + assert.deepStrictEqual(res.headersDistinct, Object.assign({ __proto__: null }, { 'x-res-a': [ 'AAA', 'BBB', 'CCC' ], 'x-res-b': [ 'DDD; EEE; FFF; GGG' ], 'connection': [ 'close' ], 'x-res-c': [ 'HHH', 'III' ], 'x-res-d': [ 'JJJ; KKK; LLL' ], - 'transfer-encoding': [ 'chunked' ] - }); + 'transfer-encoding': [ 'chunked' ], + })); res.on('end', function() { assert.deepStrictEqual(res.rawTrailers, [ @@ -153,7 +153,7 @@ server.listen(0, common.mustCall(() => { ); assert.deepStrictEqual( res.trailersDistinct, - { 'x-res-x': ['XXX', 'YYY'], 'x-res-y': ['ZZZ; WWW'] } + Object.assign({ __proto__: null }, { 'x-res-x': ['XXX', 'YYY'], 'x-res-y': ['ZZZ; WWW'] }) ); server.close(); }); -- 2.30.2